﻿using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;

namespace block_explorer
{
    class DirectionalLight : BaseSceneLight
    {
        public Vector3 Direction;

        private const int NUM_CASCADES = 3;

        private RenderTarget2D mShadowMap;
        private int mCascadeWidth = 1024;
        private int mCascadeHeight = 1024;

        private float[] mClipPlanes;
        private Matrix[] mViewMatrices;
        private Matrix[] mProjectionMatrices;
        private Matrix[] mInverseProjectionMatrices;

        public float[] ClipPlanes { get { return mClipPlanes; } }
        public Matrix[] Views { get { return mViewMatrices; } }
        public Matrix[] Projections { get { return mProjectionMatrices; } }
        public Matrix[] InverseProjections { get { return mInverseProjectionMatrices; } }
        public float CascadeWidth { get { return 1 / (float)NUM_CASCADES; } }

        public DirectionalLight()
        {
            Direction = Vector3.One;
        }

        public DirectionalLight(Vector3 direction, Color color)
            :base(color)
        {
            Direction = direction;
        }

        public override void PrepareShadowMap(GraphicsDevice device)
        {
            mClipPlanes = new float[NUM_CASCADES+1];
            mViewMatrices = new Matrix[NUM_CASCADES];
            mProjectionMatrices = new Matrix[NUM_CASCADES];
            mInverseProjectionMatrices = new Matrix[NUM_CASCADES];
            mShadowMap = new RenderTarget2D(device, mCascadeWidth * NUM_CASCADES, mCascadeHeight, false, SurfaceFormat.Single, DepthFormat.Depth24);
        }

        public override void DrawShadowMap(GraphicsDevice device, Shaders.ShadowMapEffect occlusionEffect, Scene scene, SceneCamera camera)
        {
            BoundingFrustum viewFrustum = camera.Frustum;
            Vector3[] viewFrustumCorners = viewFrustum.GetCorners();
            Vector3[] viewSpaceCorners = new Vector3[8];
            Matrix viewMatrix = camera.View;
            Vector3.Transform(viewFrustumCorners, ref viewMatrix, viewSpaceCorners);

            mClipPlanes[0] = camera.Near;
            mClipPlanes[mClipPlanes.Length - 1] = camera.Far;

            for (int i = 1; i < mClipPlanes.Length - 1; ++i)
                mClipPlanes[i] = camera.Near + (camera.Far - camera.Near) * (float)Math.Pow((i / (float)NUM_CASCADES), 2);

            Viewport splitViewport = new Viewport();
            
            BoundingFrustum frustum = new BoundingFrustum(Matrix.Identity);

            device.SetRenderTarget(mShadowMap);
            occlusionEffect.Technique = Shaders.ShadowMapEffect.ShadowTechnique.Directional;
            occlusionEffect.Passes[0].Apply();

            for (int i = 0; i < NUM_CASCADES; i++)
            {
                float lightNear = mClipPlanes[i];
                float lightFar = mClipPlanes[i + 1];

                Camera lightCamera = CreateLightCamera(viewSpaceCorners, lightNear, lightFar, camera);
                mViewMatrices[i] = lightCamera.View;
                mProjectionMatrices[i] = lightCamera.Projection;
                mInverseProjectionMatrices[i] = Matrix.Invert(lightCamera.Projection);

                // Set the viewport for the current split     
                splitViewport.MinDepth = 0;
                splitViewport.MaxDepth = 1;
                splitViewport.Width = mCascadeWidth;
                splitViewport.Height = mCascadeHeight;
                splitViewport.X = i * mCascadeWidth;
                splitViewport.Y = 0;
                device.Viewport = splitViewport;

                scene.Draw(occlusionEffect, lightCamera);
            }
        }

        private Camera CreateLightCamera(Vector3[] viewSpaceFrustumCorners, float nearZ, float farZ, SceneCamera camera)
        {
            // generate the 8 corners of the clipped frustum
            Vector3[] clippedCorners = new Vector3[8];

            for (int i = 0; i < 4; ++i)
                clippedCorners[i] = viewSpaceFrustumCorners[4 + i] * (nearZ / camera.Far);

            for (int i = 4; i < 8; ++i)
                clippedCorners[i] = viewSpaceFrustumCorners[i] * (farZ / camera.Far);

            // translate the corners into world space
            Matrix invertView = Matrix.Invert(camera.View);
            Vector3.Transform(clippedCorners, ref invertView, clippedCorners);

            // translate the corners to match the light direction so we can get the axis aligned bounding box
            Vector3 up = Vector3.Up;
            if (Math.Abs(Vector3.Dot(up, Direction)) > 0.9f)
                up = Vector3.Forward;
            Matrix rotMatrix = Matrix.CreateLookAt(Vector3.Zero, -Direction, up);
            Vector3.Transform(clippedCorners, ref rotMatrix, clippedCorners);

            // create the box from the frustum corners
            BoundingBox box = BoundingBox.CreateFromPoints(clippedCorners);

            // light position at the center of the rear of the box
            Vector3 lightPosition = box.Min;
            lightPosition.X += (box.Max.X - box.Min.X) * 0.5f;
            lightPosition.Y += (box.Max.Y - box.Min.Y) * 0.5f;
            lightPosition.Z -= 20.0f;

            // translate back into world space
            lightPosition = Vector3.Transform(lightPosition, Matrix.Invert(rotMatrix));

            // create the camera and its view/projections
            Camera c = new Camera();
            c.View = Matrix.CreateLookAt(lightPosition, lightPosition + Direction, up);
            c.Projection = Matrix.CreateOrthographic(box.Max.X - box.Min.X, box.Max.Y - box.Min.Y, 0.01f, 200.0f);

            return c;
        }



        public override Texture ShadowMap { get { return mShadowMap; } }
    }
}
